package com.openswimsoftware.orm;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;

import com.openswimsoftware.orm.model.Model;
import com.openswimsoftware.orm.model.annotation.Ignore;
import com.openswimsoftware.orm.model.annotation.Parent;
import com.openswimsoftware.orm.model.exception.InvalidModelField;
import com.openswimsoftware.orm.model.exception.InvalidModelRelationship;
import com.openswimsoftware.orm.model.list.ModelList;
import com.openswimsoftware.orm.type.ColumnType;

import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;

public class ORM {
	public static String DATABASE_NAME = "orm";
	public static int DATABASE_VERSION = 1;
	public static String DATABASE_CREATE_CLASS = "";
	
	private static Class<?>[] DATA_TYPES = {
		Byte.TYPE, Short.TYPE, Integer.TYPE, Long.TYPE, 
		Float.TYPE, Double.TYPE, Character.TYPE, 
		Boolean.TYPE, String.class, Date.class
	};
	
	public static void configureORM(String databaseCreateClass, String databaseName, int version) 
	{
		// FIX ORM configuration should be verified here
		DATABASE_CREATE_CLASS = databaseCreateClass;
        DATABASE_NAME = databaseName;
        DATABASE_VERSION = version;
	}
	
	public static void onCreate(SQLiteDatabase db, Class<?> modelType) 
	{
		String tableCreate = "create table ";
		tableCreate += ORM.tableName(modelType);
		tableCreate += "( _ID INTEGER PRIMARY KEY AUTOINCREMENT";
		
		for (Field field: ORM.getDataFields(modelType))
		{
			String fieldName = ORM.fieldName(field);
			
			if ( ! fieldName.equals("_ID"))
			{
				tableCreate += ", " + fieldName + " " + ORM.columnType(field).toString();
			}
		}
		for (Field field: ORM.getRelationshipFields(modelType))
		{
			if (field.isAnnotationPresent(Parent.class))
			{
					tableCreate += ", " + ORM.fieldName(field) + " INTEGER";
			}
		}
		
		tableCreate += ");";
		
		Log.d("ORM", "creating table: " + tableCreate);
		
		db.execSQL(tableCreate);
	}
	
	public static void onUpgrade(SQLiteDatabase db, int oldVer, int newVer, Class<?> modelType) 
	{
		Log.d("ORM", "Dropping table: " + ORM.tableName(modelType));
		
		db.execSQL("DROP TABLE IF EXISTS " + ORM.tableName(modelType));
		
		ORM.onCreate(db, modelType);
	}
	
	public static String tableName(Class<?> type)
	{
		return "ORM_" + type.getSimpleName().toUpperCase(Locale.US);
	}
	
	@SuppressLint("DefaultLocale")
	public static String fieldName(Field field)
	{
		String fieldName = "_" + field.getName().toUpperCase(Locale.US);
				
		if (Model.class.isAssignableFrom(field.getType()) || ModelList.class.isAssignableFrom(field.getType()))
		{
			fieldName += "_ID";
		}
		return fieldName;
	}
	
	public static ColumnType columnType(Field field)
	{
		Class<?> type = field.getType();
		
		if (type == Byte.TYPE || type == Byte.class)
		{
			return ColumnType.INTEGER;
		}
		else if (type == Short.TYPE || type == Short.class)
		{
			return ColumnType.INTEGER;
		}
		else if (type == Integer.TYPE || type == Integer.class)
		{
			return ColumnType.INTEGER;
		}
		else if (type == Long.TYPE || type == Long.class)
		{
			return ColumnType.INTEGER;
		}
		else if (type == Float.TYPE || type == Float.class)
		{
			return ColumnType.REAL;
		}
		else if (type == Double.TYPE || type == Double.class)
		{
			return ColumnType.REAL;
		}
		else if (type == Character.TYPE || type == Character.class)
		{
			return ColumnType.TEXT;
		}
		else if (type == Boolean.TYPE || type == Boolean.class)
		{
			return ColumnType.INTEGER;
		}
		else if (type == Date.class)
		{
			return ColumnType.NUMERIC;
		}
		else
		{
			return ColumnType.TEXT;
		}
	}
	
	public static Field[] getDataFields(Class<?> modelType)
	{
		List<Field> dataFields = new ArrayList<Field>();
		for (Field field: ORM.getFields(modelType))
		{
			Log.d("orm", "Checking data field " + field.getName());
			if ( ! field.isAnnotationPresent(Ignore.class))
			{
				if ( ! ORM.exclude(field))
				{
					if (ORM.isDataType(field.getType()))
					{
						Log.d("orm", "Adding data field");
						dataFields.add(field);
					}
				}
				else
				{
					Log.d("orm", "Field is excluded");
				}
			}
			else
			{
				Log.d("orm", "Field is ignored");
			}
		}
		
		return dataFields.toArray(new Field[dataFields.size()]);
	}
	
	public static  Field[] getRelationshipFields(Class<?> modelType)
	{
		List<Field> relationshipFields = new ArrayList<Field>();
		for (Field field: ORM.getFields(modelType))
		{
			Log.d("orm", "Checking relationship field " + field.getName());
			if ( ! field.isAnnotationPresent(Ignore.class))
			{
				if (Model.class.isAssignableFrom(field.getType()) || ModelList.class.isAssignableFrom(field.getType()))
				{
					relationshipFields.add(field);
				}
				else
				{
					Log.d("orm", "Field not of a sub class of Model or Model list");
				}
			}
			else
			{
				Log.d("orm", "Field is ignored");
			}
		}
		
		return relationshipFields.toArray(new Field[relationshipFields.size()]);
	}
	
	public static Field getDataField(Class<?> modelType, String fieldName) throws InvalidModelField
	{
		for (Field field: ORM.getDataFields(modelType))
		{
			if (field.getName().equals(fieldName))
			{
				return field;
			}
		}
		
		throw new InvalidModelField(fieldName, modelType);
	}
	
	public static Field getField(Class<?> modelType, String fieldName) throws InvalidModelField
	{
		for (Field field: ORM.getDataFields(modelType))
		{
			if (field.getName().equals(fieldName))
			{
				return field;
			}
		}
		
		for (Field field: ORM.getRelationshipFields(modelType))
		{
			if (field.getName().equals(fieldName))
			{
				return field;
			}
		}
		
		throw new InvalidModelField(fieldName, modelType);
	}
	
	public static ContentValues putFieldData(ContentValues values, Field field, Model<?> model) throws IllegalArgumentException, IllegalAccessException
	{
		Class<?> type = field.getType();
		Log.d("orm", "Field type is " + type.getSimpleName());
		
		field.setAccessible(true);
		
		if (type == Byte.TYPE || type == Byte.class)
		{
			values.put(ORM.fieldName(field), field.getByte(model));
		}
		else if (type == Short.TYPE || type == Short.class)
		{
			values.put(ORM.fieldName(field), field.getShort(model));
		}
		else if (type == Integer.TYPE || type == Integer.class)
		{
			values.put(ORM.fieldName(field), field.getInt(model));
		}
		else if (type == Long.TYPE || type == Long.class)
		{
			values.put(ORM.fieldName(field), field.getLong(model));
		}
		else if (type == Float.TYPE || type == Float.class)
		{
			values.put(ORM.fieldName(field), field.getFloat(model));
		}
		else if (type == Double.TYPE || type == Double.class)
		{
			values.put(ORM.fieldName(field), field.getDouble(model));
		}
		else if (type == Boolean.TYPE || type == Boolean.class)
		{
			values.put(ORM.fieldName(field), field.getBoolean(model));
		}
		else if (type == Date.class)
		{
			values.put(ORM.fieldName(field), ((Date) field.get(model)).getTime());
		}
		else
		{
			values.put(ORM.fieldName(field), field.get(model).toString());
		}
		
		return values;
	}
	
	public static Object getFieldData(Cursor cursor, Field field, Model<?> model)
	{
		Class<?> type = field.getType();
		Log.d("orm", "Getting field " + ORM.fieldName(field) + " of type " + type.getSimpleName());
		
		int index = cursor.getColumnIndex(ORM.fieldName(field));
		
		Object data;
		
		field.setAccessible(true);
		
		if (type == Byte.TYPE)
		{
			data = (byte) cursor.getInt(index);
		}
		else if (type == Byte.class)
		{
			data = Byte.valueOf((byte) cursor.getInt(index));
		}
		else if (type == Short.TYPE)
		{
			data = cursor.getShort(index);
		}
		else if (type == Short.class)
		{
			data = Short.valueOf(cursor.getShort(index));
		}
		else if (type == Integer.TYPE)
		{
			data = cursor.getInt(index);
		}
		else if (type == Integer.TYPE)
		{
			data = Integer.valueOf(cursor.getInt(index));
		}
		else if (type == Long.TYPE)
		{
			data = cursor.getLong(index);
		}
		else if (type == Long.class)
		{
			data = Long.valueOf(cursor.getLong(index));
		}
		else if (type == Float.TYPE)
		{
			data = cursor.getFloat(index);
		}
		else if (type == Float.class)
		{
			data = Float.valueOf(cursor.getFloat(index));
		}
		else if (type == Double.TYPE)
		{
			data = cursor.getDouble(index);
		}
		else if (type == Double.class)
		{
			data = Double.valueOf(cursor.getDouble(index));
		}
		else if (type == Boolean.TYPE)
		{
			if (cursor.getInt(index) > 0)
			{
				data = true;
			}
			else
			{
				data = false;
			}
		}
		else if (type == Boolean.class)
		{
			if (cursor.getInt(index) > 0)
			{
				data = Boolean.valueOf(true);
			}
			else
			{
				data = Boolean.valueOf(false);
			}
		}
		else if (type == Date.class)
		{
			Calendar calendar = Calendar.getInstance();
	        calendar.setTimeInMillis(cursor.getLong(index));
	        data = calendar.getTime();
		}
		else
		{
			data = cursor.getString(index);
		}
		
		return data;
	}
	
	public static Field findParent(Class<?> modelType) throws InvalidModelRelationship
	{
		for (Field field: ORM.getRelationshipFields(modelType))
		{
			if (field.isAnnotationPresent(Parent.class))
			{
				return field;
			}
		}
		
		throw new InvalidModelRelationship(modelType);
	}
	
	private static Field[] getFields(Class<?> modelType)
	{
		List<Field> fields = new ArrayList<Field>();
		
		while (Model.class.isAssignableFrom(modelType))
		{
			fields.addAll(Arrays.asList(modelType.getDeclaredFields()));
			modelType = modelType.getSuperclass();
		}
			
		return fields.toArray(new Field[fields.size()]);
	}
	
	private static boolean exclude(Field field)
	{
		return (Modifier.isFinal(field.getModifiers()) || Modifier.isStatic(field.getModifiers()));
	}
	
	private static boolean isDataType(Class<?> type)
	{
		Log.d("orm", "checking field type against data types");
		Log.d("orm", "looking for type: " + type.getSimpleName());
		for (Class<?> dataType: ORM.DATA_TYPES)
		{
			Log.d("orm", "checking against type: " + dataType.getSimpleName());
			if (type == dataType)
			{
				return true;
			}
		}
		
		return false;
	}
	
}
